FreeBSD on the Lenovo Thinkpad X201
After many many years of running Linux on my Thinkpad X201 I decided to move over to FreeBSD.
Unlike a Linux install where everything auto-configured and worked out of the box, installing FreeBSD on a laptop in 2024 is very much like installing Linux back in the late 2000's. The basics are there but you need some tweaks to make it actually usable. So I created this page to document what needs to be done, both for my future reference and so that anyone else coming across these issues doesn't have to debug everything from scratch.
Some of these bits of information are also to assist long time Linux users to move to FreeBSD. While they have their similarities they also have their differences, which I will document here in relation to the X201.
Note that I don't cover the basics like installing packages, etc... The assumption is that you are comfortable with Unix operating systems already and for FreeBSD basics I can recommend the FreeBSD Handbook.
The install
As this is a laptop I take outside all the time the chances it gets lost or stolen are higher than a desktop, I went for a ZFS Encrypted zpool.
The install itself was flawless, and after about 20 mins (mostly downloading packages) I had a bootable FreeBSD OS.
Setting up X11
This also proved to be simple. In fact it was so simple all I had to do was install "xinit" and run "startx". For the purposes of being explicit I did generate an xorg.conf file with the "X -configure" and saved it to /etc/X11/xorg.conf, but it is not required.
With that done I had a basic X11 session running. I went a bit further and installed fluxbox (my preferred window manager on small screens), configured my .xinitrc and went on my merry way.
Setting up suspend/resume
The second thing I tried to use was suspend/resume. On FreeBSD you type "acpiconf -s 3" on the command line to suspend the machine to RAM (if you want to suspend to disk, you run "acpiconf -s 4"). This bit worked fine, however when the machine resumed the screen remained off. The machine worked fine and I could SSH into it, but without the screen on I could not actually use it.
After quite a bit of research and a lot of help from the folks over at forums.freebsd.org I discovered that you have to load the Intel Graphics driver in order for this to work. By default FreeBSD has the VESA driver configured which is why my X11 session worked without any special driver install, but for more advanced things like suspend/resume you need the official driver.
Interestingly the FreeBSD community decided that instead of writing their own drivers all over again, they would implement a shim for the Linux DRM interface. This allows you to use Linux graphics drivers in FreeBSD.
To make use of this you have to install the "drm-kmod" package, once that is done you add your driver to the kld_list variable in /etc/rc.conf, as so:
kld_list="i915kms"
You can manually load up the driver using "kldload i915kms". If it all works as intended your console will change its graphics. You will go from the more traditional 80 column large font UNIX console to much smaller font console, akin to the Linux framebuffer console.
Interestingly I did not need to alter the xorg.conf. Its default Driver is "modesetting", which will automatically use the best available graphics driver. All I had to do was "startx" again to get my X session running.
Now suspend/resume worked perfectly.
Setting suspend on lid-close
If you are like me and like your laptop to suspend to RAM when you close the lid, you need to set the sysctl, as so:
root@Io~# sysctl hw.acpi.lid_switch_state=S3 hw.acpi.lid_switch_state: NONE -> S3
To make it permanent you put a line "hw.acpi.lid_switch_state=S3" in your /etc/sysctl.conf file.
Now when you close/open the lid the machine goes to sleep/resume. It is nice that this is configurable in sysctl on FreeBSD because it allows you to dynamically change the setting. So if by default you set it to suspend on lid close, but on one occasion you want to disable this, just change the above back to "NONE" (as root). It will persist until changed again or you reboot, after which whatever you set in /etc/sysctl.conf is set again.
Hibernate
To go into hibernate you need to run "acpiconf -s4". Unfortunately it doesn't currently work on my x201. The machine powers off but when you power on again it is a clean instance, you've lost all your state. This is inconvenient for me as on Linux the x201 would auto-hibernate when battery was below 5%, allowing me to swap batteries and quickly resume my work.
Power and battery
To get information on your battery and its current state, you use "acpiconf -i $batt_number", where $batt_number which battery you want information from (on the x201 you only have one battery, so this is '0', some of the larger thinkpads had an option of two batteries).
#$ acpiconf -i 0 Design capacity: 73260 mWh Last full capacity: 38820 mWh Technology: secondary (rechargeable) Design voltage: 11100 mV Capacity (warn): 1941 mWh Capacity (low): 200 mWh Low/warn granularity: 1 mWh Warn/full granularity: 1 mWh Model number: 08K8193 Serial number: 15 Type: LION OEM info: JingYi State: discharging Remaining capacity: 94% Remaining time: 1:58 Present rate: 18569 mW Present voltage: 11760 mV
As you can see from above my "Design capacity" and "Last full capacity" are very different, my battery only has about 50% of its original capacity which makes sense considering that it is very old at this point. Still even at 50% capacity I get around 2h of use with the backlight at 100% (which is the largest power draw on the laptop).
Backlight control
On Linux the backlight was easily controlled by the fn+HOME/END keys as this was actually handled by the BIOS and not Linux
On FreeBSD however it does not work. It looks like the FreeBSD actually takes control of these keys from the BIOS so they no longer work.
First thing was to find out how to set the backlight in software, so that I can at least control it. I found out that there is an aptly named program called "backlight", which does the trick:
root@Io:~ # backlight brightness: 29 root@Io:~ # backlight -h Usage: backlight [-q] [-f device] backlight [-q] [-f device] -i backlight [-f device] value backlight [-f device] incr|+ value backlight [-f device] decr|- value
Simple enough and does the job. As an added bonus it does not require root. From what I can see FreeBSD exposes the backlight control in the "/dev/backlight" folder, allowing the backlight program to control brightness on the console or X11 as any user.
Binding the brightness keys
While having backlight control again is nice it is purely software controlled. This means I can't make use of the buttons without some configuration. First thing I need to do is enable the buttons so that they are registered by the OS. In order to do this you need to load the "acpi_video" module. A simple "kldload acpi_video" will suffice.
If it worked you would get no errors, and sysctl will now have some acpi_video options, as so:
dev.acpi_video.0.%parent: vgapci0 dev.acpi_video.0.%pnpinfo: dev.acpi_video.0.%location: dev.acpi_video.0.%driver: acpi_video dev.acpi_video.0.%desc: ACPI video extension dev.acpi_video.%parent:
Now you should have working keys. You can test by running "xev". If all goes as planned, when you hit fnCTRL + KeyUP/Down you should get responses corresponding to the below:
KeyPress event, serial 33, synthetic NO, window 0xc00001, root 0x1cf, subw 0x0, time 42811377, (123,94), root:(945,120), state 0x0, keycode 233 (keysym 0x1008ff02, XF86MonBrightnessUp), same_screen YES, XLookupString gives 0 bytes: XmbLookupString gives 0 bytes: XFilterEvent returns: False KeyRelease event, serial 33, synthetic NO, window 0xc00001, root 0x1cf, subw 0x0, time 42811377, (123,94), root:(945,120), state 0x0, keycode 233 (keysym 0x1008ff02, XF86MonBrightnessUp), same_screen YES, XLookupString gives 0 bytes: XFilterEvent returns: False KeyPress event, serial 36, synthetic NO, window 0xc00001, root 0x1cf, subw 0x0, time 42811846, (123,94), root:(945,120), state 0x0, keycode 232 (keysym 0x1008ff03, XF86MonBrightnessDown), same_screen YES, XLookupString gives 0 bytes: XmbLookupString gives 0 bytes: XFilterEvent returns: False KeyRelease event, serial 36, synthetic NO, window 0xc00001, root 0x1cf, subw 0x0, time 42811846, (123,94), root:(945,120), state 0x0, keycode 232 (keysym 0x1008ff03, XF86MonBrightnessDown), same_screen YES, XLookupString gives 0 bytes: XFilterEvent returns: False
If you prefer to use xbindkeys, you can run "xbindkeys -k" and get the following output:
"(Scheme function)" m:0x0 + c:233 XF86MonBrightnessUp "(Scheme function)" m:0x0 + c:232 XF86MonBrightnessDown
Whichever tool you use, you can see the key code combinations you need to set. If you use xbindkeys then its easy, just replace "(Scheme function)" lines above with "brightness + 5" and "brightness - 5" respectively (if you want 5 step change of brightness on button press, otherwise change to taste).
As I am using fluxbox on this machine it has its own keybinding server. To make use of that to control brightness, we have to alter the $HOME/.fluxbox/keys file, to add the following lines:
232 :Exec backlight - 5 233 :Exec backlight + 5
Then reload your fluxbox config (no need to restart your session), and test the buttons. In my case it worked, so success! I went with a 5% step each button press. This gives me 20 steps between 0 and 100% which for me is a good balance. You can set any valid value you like, the larger the value the fewer steps you have and the greater the variance between steps.
Other IBM/Lenovo thinkpad features
There is one other module available, called "acpi_ibm". This one exposes more features of the laptop to userspace control. To load it you "kldload acpi_ibm", and once done you get the following new sysctl fields:
dev.acpi_ibm.0.handlerevents: NONE dev.acpi_ibm.0.mic_led: 0 dev.acpi_ibm.0.fan: 1 dev.acpi_ibm.0.fan_level: 0 dev.acpi_ibm.0.fan_speed: 2592 dev.acpi_ibm.0.wlan: 1 dev.acpi_ibm.0.bluetooth: 1 dev.acpi_ibm.0.thinklight: 1 dev.acpi_ibm.0.mute: 1 dev.acpi_ibm.0.volume: 7 dev.acpi_ibm.0.lcd_brightness: 0 dev.acpi_ibm.0.hotkey: 2486 dev.acpi_ibm.0.eventmask: 134217727 dev.acpi_ibm.0.events: 1 dev.acpi_ibm.0.availmask: 134217727 dev.acpi_ibm.0.initialmask: 2060 dev.acpi_ibm.0.%parent: acpi0 dev.acpi_ibm.0.%pnpinfo: _HID=IBM0068 _UID=0 _CID=none dev.acpi_ibm.0.%location: handle=\_SB_.PCI0.LPC_.EC__.HKEY dev.acpi_ibm.0.%driver: acpi_ibm dev.acpi_ibm.0.%desc: ThinkPad ACPI Extras
Some of these (like dev.acpi_ibm.0.fan_speed) are read only, but can be useful to for monitoring. Others like dev.acpi_ibm.0.fan can be set (in this case, you can disable or enable your fan). These override the bios, so for example if you set dev.acpi_ibm.0.fan=0 your fan will turn off and it won't turn on no matter how hot the machine gets. Beyond 100°C your CPU will just shut off.
This means some of these options can damage your laptop and should only be used if you know what you are doing. In my case I quite like the ability to script the turning on/off of the thinklight using sysctl. I can incorporate it as an indicator in some of my scripts.
Current settings
So with all of the above, this is how my current settings look to run FreeBSD on the X201:
# In /etc/rc.conf: kld_list="i915kms acpi_video acpi_ibm"
# In /etc/sysctl.conf # Make the laptop suspend to RAM on lid close (switch to S4 to suspend to disk) hw.acpi.lid_switch_state=S3 # Make the machine more responsive (as it is a workstation rather than a server) # by increasing the context switches # see https://forums.freebsd.org/threads/what-is-sysctl-kern-sched-preempt_thresh.85601/ #kern.sched.preempt_thresh=160 #kern.eventtimer.timer=HPET
Beyond that I did one more thing. I set set the setuid bit on sysctl, as so:
chmod +s /sbin/sysctl
What this does is tell the OS that any user that calls that binary will have the binary run as the executable owner (in this case root). This allows me to issue sysctl commands as a non-root user, without resorting to things like "sudo" or similar. On a shared machine giving all normal users the ability to alter sysctl would be a security risk, however I am the only user of this machine. If any nefarious intruder got as far as accessing my encrypted volume and logging into FreeBSD, then the machine is most likely physically compromised already, so the setuid bit will be of little concern.
The advantage for setting it setuid is that I can run sysctl from my non-root user account, including in scripts, without dealing with privilege escalation.
Sound
So one surprise I found was that sound did not work "out of the box" on FreeBSD. Looking on the forums people mentioned that sound works only through the headphone jack, no luck with the internal speaker. So I decided to investigate. First thing I did was check what sound cards I have:
#$ cat /dev/sndstat Installed devices: pcm0:(play/rec) default pcm1: (play/rec) pcm2: (play) No devices installed from user space.
First surprise was that I actually had three built in sound cards. I then used the "beep" utility to test each one out. I discovered that each sound card is for a specific output, details in the comments:
#$ beep -g 100 -d /dev/dsp0.0 # Headphones #$ beep -g 100 -d /dev/dsp1.0 # Speakers #$ beep -g 100 -d /dev/dsp2.0 # Dock stereo speakers
I also found that if the headphones are plugged in the internal speaker is muted, in such a case unless you send output to the "headphones" sound card you will hear nothing on either headphones or speakers. In addition the acpi_ibm kernel module gives you the following sound controls:
dev.acpi_ibm.0.mute: 1 # Boolean for mute/unmute dev.acpi_ibm.0.volume: 14 # Volume range is from 0 to 14
The "mute" is controlled by the physical mute button on the keyboard. It acts only on the speakers. The headphone jack is unaffected. The physical volume buttons have been taken over by the operating system so out of the box they do nothing. They are visible on xev as so:
KeyPress event, serial 36, synthetic NO, window 0x3800001, root 0x1cf, subw 0x0, time 19824318, (665,480), root:(667,506), state 0x0, keycode 122 (keysym 0x1008ff11, XF86AudioLowerVolume), same_screen YES, XLookupString gives 0 bytes: XmbLookupString gives 0 bytes: XFilterEvent returns: False KeyRelease event, serial 36, synthetic NO, window 0x3800001, root 0x1cf, subw 0x0, time 19824446, (665,480), root:(667,506), state 0x0, keycode 122 (keysym 0x1008ff11, XF86AudioLowerVolume), same_screen YES, XLookupString gives 0 bytes: XFilterEvent returns: False KeyPress event, serial 36, synthetic NO, window 0x3800001, root 0x1cf, subw 0x0, time 19825195, (665,480), root:(667,506), state 0x0, keycode 123 (keysym 0x1008ff13, XF86AudioRaiseVolume), same_screen YES, XLookupString gives 0 bytes: XmbLookupString gives 0 bytes: XFilterEvent returns: False KeyRelease event, serial 36, synthetic NO, window 0x3800001, root 0x1cf, subw 0x0, time 19825354, (665,480), root:(667,506), state 0x0, keycode 123 (keysym 0x1008ff13, XF86AudioRaiseVolume), same_screen YES, XLookupString gives 0 bytes: XFilterEvent returns: False
So once again we can bind these to do what we want. To get it to work on the volume in sysctl as before will need some script work. So I wrote this quick perl script:
#!/usr/bin/env perl $cmd = shift; if($cmd eq "up") { setVol(getVol() + 1); } elsif ($cmd eq "down") { setVol(getVol() - 1); } else { die("Unknown cmd $cmd\n"); } sub getVol(){ return `sysctl -n dev.acpi_ibm.0.volume`; } sub setVol() { if ($_[0] < 0) { return; } # Can't go below 0 die("Failed to set volume: $!\n") if system( "sysctl", "dev.acpi_ibm.0.volume=$_[0]" ); }
I named the script "volume", put it in my $PATH and set it executable. Then in my fluxbox/.keys file I put
122 :Exec volume down 123 :Exec volume up
And with that the volume keys do what they should. For the level of complexity it may be easier to see if you can get FreeBSD to not take over these keys, as then I suspect they will operate the volume directly. This however does give you the flexibility to customise what the buttons do.